Software I2C: hold the data line until the clock is actually down - #261
Merged
Conversation
The software I2C moved SDA in the same breath as pulling SCL low. A data line that changes while the clock still reads high is a start or a stop to every device on the bus, so the transfer ended instead of carrying a bit and no device ever acknowledged its address. Wait for each line to reach the level it was just given, rather than budgeting a fixed time for it: the clock is driven low, so waiting for it costs the fall time of the bus and nothing more, and the released data line is given the time its own rise actually takes. A fixed hold would have to come out of the setup time, which is what the slow rise of a released line needs at the higher clock rates. The same wait now precedes the start, the repeated start and the stop, where the rise of the released data line decides whether the condition appears on the bus at all. A line that never reaches its level is reported the way a clock that will not rise already was, so the transfer ends instead of carrying on with a bus that is not there. The recovery path is the one exception: a clock that will not settle is the condition it exists to clear. An acknowledge needs one more distinction. This master drives the data line low for the bit before it, so a low reading is either a device holding the line or a rise that has not finished. A device holds it for the whole pulse while a rise is over within the time the bus is allowed to take for one, so the level is read again to separate them.
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The software I2C reached over the negative port numbers never received an
acknowledge from any device. The transfer looked well formed on a logic level —
start, eight bits, a released line for the ninth clock — but nothing answered,
so a bus with a device on it was indistinguishable from an empty one.
Cause
soft_i2c_write_byte()set the data line in the same breath as pulling theclock low:
Both are register writes a few cycles apart. The data line therefore moves
while the clock can still read high, and a data change with the clock high is
a start or a stop to every device on the bus. The device sees the frame torn
up rather than a bit going by, and never gets as far as the address it was
being asked about.
Measured
An ES8311 on an ESP32-S3, probed at its own address:
The same address answers a plain bit-banged probe written alongside it, so the
device and the wiring were never in question.
Fix
Wait for each line to reach the level it was just given, instead of budgeting a
fixed time for it.
nothing more: nothing on a light bus, more on a loaded one.
the part that is slow and the part a fixed budget gets wrong on a loaded bus.
A fixed hold would have to come out of the setup time, and the setup time is
exactly what the slow rise of a released line needs once the clock rate goes
up. Measured with this fix in place, the probe answers at every setting the
port offers, including the fastest one where no fixed waits remain at all:
The same wait now precedes the start, the repeated start and the stop, where
the rise of the released data line decides whether the condition appears on the
bus at all — without it, a start could be issued while the line was still on
its way up, and a healthy bus could be mistaken for one that another device is
holding, which triggered the recovery clocks for no reason.
A line that never reaches its level is now reported, the way a clock that will
not rise already was, so a transfer ends instead of carrying on over a bus that
is not there. The recovery path is the one deliberate exception: a clock that
will not settle is the condition it exists to clear.
The acknowledge needs one distinction beyond that. This master drives the data
line low for the bit before it, so a low reading is either a device holding the
line or a rise that has not finished. A device holds it for the whole pulse
while a rise is over within the time the bus is allowed to take for one, so the
level is read a second time to separate the two. Without this, a port running
with no fixed waits reports an acknowledge from a bus that has no device on it.
Verified
board identification: every board reports the address that is actually on its
bus, and none reports one that is not.
every address on every board.
Known limits, unchanged by this
init(),setPins()andrelease()do not take the slot lock and do notlook at whether a transfer is open. That predates this change and is left
alone here.
specification allows, not by a measurement of the bus in hand. A bus slower
than the specification permits is still read as an acknowledge that is not
there.